Android进程的so注入--Poison(稳定注入版)

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/53869796

Android进程的so注入已经是老技术了,网上能用的Android注入的工程也有很多,虽然分享代码的作者在测试的时候能注入成功,但是其他的同学使用这些代码的时候总是出现这样或者那样的问题。在Android逆向学习的这段时间里,我也陆续测试了几个作者给出的Android的注入的代码,但是总是效果不明显,今天就学习一下大牛boyliang分享的Android的so注入的代码框架Poison,作者boyliang的注入代码也是基于大牛古河分享的Andorid注入的代码修改过来的,做了一些改进和优化,后面的文章中就对注入代码进行学习一下。


一、注入工程Poison-master代码的下载

1)作者boyliang的注入代码的原下载地址已经失效了,但是从github上还是可以查找的,下载地址为:https://github.com/matrixhawk/Poison(缺少编译的Android.mk文件和Application.mk配置文件,需要自己编写)。

windows环境下,NDK编译需要添加的include头文件(根据编译的版本需要进行修改)
右击项目 --> Properties --> 左侧C/C++ General --> Paths and Symbols --> 右侧Includes --> GNU C++(.cpp) --> Add
${NDKROOT}platformsandroid-19arch-armusrinclude
${NDKROOT}sourcescxx-stlgnu-libstdc++4.8include 
${NDKROOT}sourcescxx-stlgnu-libstdc++4.8libsarmeabiinclude
${NDKROOT} oolchainsarm-Linux-androideabi-4.8prebuiltwindowslibgccarm-linux-androideabi4.8include


2)为Poison-master工程添加编译需要的Android.mk文件和Application.mk文件如下:

Android.mk文件:

LOCAL_PATH := $(call my-dir)  
  
include $(CLEAR_VARS)  

# 编译生成的模块的名称
LOCAL_MODULE := poison  

# 需要被编译的源码文件 
LOCAL_SRC_FILES := poison.c 
	       elf_utils.c 
	       ptrace_utils.c 
	       tools.c 

# 支持log日志打印android/log.h里函数调用的需要
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog  

# 编译模块生成可执行文件 
include $(BUILD_EXECUTABLE)  
Application.mk文件:

# 编译生成的模块运行支持的平台
APP_ABI := armeabi-v7a  
# 设置编译连接的工具的版本
#NDK_TOOLCHAIN_VERSION = 4.9 


3)即便是如此,使用NDK编译源码工程Poison-master,仍然会提示“jni/ptrace_utils.c:12:28: fatal error: cutils/sockets.h: No such file or directory”等的错误,这个问题怎么解决呢?作者boyliang已经为我们解决好了这个问题,需要对Android官方提供的NDK工具进行path,添加源码编译需要的一些系统的头文件,需要添加的Android系统头文件的ndk-patch可以从作者的github地址:https://github.com/boyliang/ndk-patch进行下载。使用的用法如下:



4)哈哈,编译成功了,Android进程so注入的工具就有了。




二、注入工程代码Poison的说明

1)关于Android的so注入的详细的原理和细节,可以参考博文http://blog.csdn.net/qq1084283172/article/details/46859931,这篇博文里已经把Android的so注入的代码分析的很清楚了,基本把古河大牛的LibInject都说明白了。古河大牛的LibInject中涉及到了Android函数的Hook部分的模板的编写,作者boyliang的代码中没有涉及到Android函数的Hook部分的代码,这部分后面再研究,大牛boyliang和古河的so注入部分的思路是一样的,只不过boyliang的so注入代码中考虑到了"zygote"进程注入的特殊情况,在对进程目标pid进程ptrace时,有着特殊的处理。





2)Andorid的so注入时,针对"zygote"进程注入的特殊处理的原因,可以参考《Android so注入》这篇博文给出的解释原因。

A.针对"zygote"进程注入时,so库文件必须存放在“/system/lib/“路径下。



B.针对"zygote"进程注入时,ptrace操作"zygote"进程时的特殊处理(直接搬过来)。


可以看到ptrace_attach只是对ptrace(PTRACE_ATTACH,…)做了一个封装,但是在attach还做了一系列的waitpid和ptrace(PTRACE_SYSCALL,…)的操作,这是为什么呢。这里我们需要复习一下ptrace的执行过程,一旦对某个进程执行了ptrace操作,那么当目标进程执行系统调用,也就是把执行的控制权交给内核的时候,内核会检查当前进程是否被标记为”traced”,如果是,那么内核就会把控制权转交给跟踪进程。而此时跟踪进程正调用了wait函数在等待内核函数的信号,当接受到信号后跟踪进程就能继续执行。但是有时候会遇到被跟踪进程执行的系统调用是一个阻塞函数,比如recv,read,这样当目标进程系统调用开始的时候(PTRACE_ATTACH在系统调用开始暂停目标进程),它就会被暂停,而跟踪进程会被唤醒,一般这个时候跟踪进程会执行ptrace(PTRACE_GETREGS,…)等操作,这需要目标进程从系统调用返回,但是目标进程这个时候已经阻塞在系统调用里面了,无法返回,ptrace就会产生错误。知道这个情况,我们就很容易理解这段代码了。首先使用PTRACE_ATTACH标记目标进程,然后等待目标进程返回,这里的WUNTRACED表示目标进程暂停后就立即返回,而不是等待目标进程结束。当目标进程进入系统调用后,通知跟踪进程,跟踪进程再调用ptrace(PTRACE_SYSCALL,…)然后等待(PTRACE_SYSCALL在目标进程进入/退出系统调用的时候暂停目标进程),表示等待目标进程进入系统调用,然后再调用一次ptrace(PTRACE_SYSCALL,…)再等待,表示等待目标进程从系统调用返回,等第三次的wait返回后((可能会被阻塞),就可以进行系统调用了。
这种做法还是有可能会被阻塞,就是第三次wait会等不到信号,也就是目标进程进入系统调用后一直不返回。什么时候会发生这种情况呢?其实zygote就是个很好的例子,这需要对zygote进程有一些了解,这里只简单的分析一下。zygote启动后会进入一个死循环,用来接收AMS的请求连接,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 * Runs the zygote process's select loop. Accepts new connections as 
 * they happen, and reads commands from connections one spawn-request's 
 * worth at a time. 
 * 
 * @throws MethodAndArgsCaller in a child process when a main() should 
 * be executed. 
 */  
private static void runSelectLoopMode() throws MethodAndArgsCaller {  
   	  ArrayList<FileDescriptor> fds = new ArrayList();
       ArrayList <ZygoteConnection> peers = new ArrayList();
       FileDescriptro[] fdArray = new FileDescriptor[4];
        ...... 
       while (true) {//死循环  
           ......  
             
           if (index < 0) {  
               throw new RuntimeException("Error in select()");  
           } else if (index == 0) {//index==0表示selcet接收到的是Zygote的socket的事件  
               ZygoteConnection newPeer = acceptCommandPeer();  
               peers.add(newPeer);  
               fds.add(newPeer.getFileDesciptor());  
           } else {//调用ZygoteConnection对象的runOnce方法,ZygoteConnection是在index == 0时被添加到peers的  
               boolean done;  
               done = peers.get(index).runOnce();  
 
               if (done) {  
                   peers.remove(index);  
                   fds.remove(index);  
               }  
           }  
       }  
	}

index变量表示此时和zygote进程通信的个数,当index=0时也就是说没有socket连接,此时zygote调用acceptCommandPeer函数,该函数等待一个连接并返回一个ZygoteConnection对象。

1
2
3
4
5
6
7
8
9
10
11
/*
 * Waits for and accepts a single command connection. Throws
 * RuntimeException on failure.
 */
private static ZygoteConnection acceptCommandPeer() {
    try {
        return new ZygoteConnection(sServerSocket.accept());
    } catch (IOException ex) {
        throw new RuntimeException("IOException during accept()", ex);
    }
}

也就是说,当没有应用启动时,zygote进程一直处于阻塞状态。所以我们上面代码中的第三次wait会无法返回,解决办法也很简单,就是主动发起一个zygote的连接。我们看到第二个waitpid后面调用了一个connect_to_zygote函数,下面是它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void* connect_to_zygote(void* arg){
	int s, len;
	struct sockaddr_un remote;
//zygote进程接收socket连接的时间间隔是500ms,2s足以保证此socket连接能连接到zygote socket
	LOGI("[+] wait 2s...");
	sleep(2);
	//sleep(0.5);
	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {
		remote.sun_family = AF_UNIX;
		strcpy(remote.sun_path, "/dev/socket/zygote");
		len = strlen(remote.sun_path) + sizeof(remote.sun_family);
		LOGI("[+] start to connect zygote socket");
		connect(s, (struct sockaddr *) &remote, len);
		LOGI("[+] close socket");
		close(s);
	}

	return NULL ;
}

这个函数的功能很简单,先发起socket连接,然后再关闭连接。看上去没有做什么有用的事情,但是它却非常重要,通过连接zygote,它使zygote进程解除了阻塞状态,我们才得以注入进zygote进程。

说明:暂时对zygote这一块不是很熟悉,参考大牛http://zke1ev3n.me/2015/12/02/Android-so注入/的分析理由。


C.针对调用目标pid进程的函数时,对于等待目标pid进程完成so注入需要注意的地方,大牛zke1ev3n也做了深入的说明和代码的补充。

ptrace_dlopen函数构造dlopen函数的参数,然后调用ptrace_call开始加载so的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
int ptrace_call(pid_t pid, uint32_t addr, long *params, int num_params, struct pt_regs* regs) {
	uint32_t i;

	for (i = 0; i < num_params && i < 4; i++) {
		regs->uregs[i] = params[i];
	}

	if (i < num_params) {
		regs->ARM_sp-= (num_params - i) * sizeof(long);
		ptrace_write(pid, (uint8_t *) regs->ARM_sp, (uint8_t *) &params[i], (num_params - i) * sizeof(long));
	}

	regs->ARM_pc= addr;
	if (regs->ARM_pc& 1) {
		/* thumb */
		regs->ARM_pc &= (~1u);
		regs->ARM_cpsr |= CPSR_T_MASK;
	} else {
		/* arm */
		regs->ARM_cpsr &= ~CPSR_T_MASK;
	}

	regs->ARM_lr= 0;	//置子程序的返回地址为空,以便函数执行完后,返回到null地址,产生SIGSEGV错误

	if (ptrace_setregs(pid, regs) == -1 || ptrace_continue(pid) == -1) {
		return -1;
	}

//	waitpid(pid, NULL, WUNTRACED);	
	
	int status = 0;
//	waitpid(pid,&stat,WUNTRACED);
  	pid_t res;
   	waitpid(pid, NULL, WUNTRACED);  
	/*
	 * Restarts  the stopped child as for PTRACE_CONT, but arranges for
	 * the child to be stopped at the next entry to or exit from a sys‐
	 * tem  call,  or  after execution of a single instruction, respec‐
	 * tively.
	 */
	if (ptrace(PTRACE_SYSCALL, pid, NULL, 0) < 0) {
		LOGE("ptrace_syscall");
		return -1;
	}

	waitpid(pid, NULL, WUNTRACED);

	if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL ) < 0) {
		LOGE("ptrace_syscall");
		return -1;
	}

	res = waitpid(pid, NULL, WUNTRACED);

	LOGI("[+] status is %x",status);
    	if (res != pid || !WIFSTOPPED (status))//WIFSTOPPED(status) 若为当前暂停子进程返回的状态,则为真
        	return 0;
	LOGI("[+]done %d
",(WSTOPSIG (status) == SIGSEGV)?1:0);
	//设置siginal 11信号处理函数
/*	if(signal(SIGSEGV,handler) == SIG_ERR){
		LOGE("[-]can not set handler for SIGSEGV");
	}*/

	return 0;
}

WUNTRACED告诉waitpid,如果子进程进入暂停状态,那么就立即返回。如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。对于使用PTRACE_CONT运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。这里的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。那么什么时候会发生这种错误呢?显然,当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV
这里还需要了解下arm架构的相关知识。首先是函数参数传递,在arm中,函数的前4个参数分别保存在r0-r3中,当参数大于4个,就依次压入栈中。此外,arm处理器实际上支持两套指令集,即arm和thumb。thumb为16位,arm为32位。这里通过判断pc的最后一位是否是1来确定指令集,这是因为编译器在用thmub指令集编译一个函数时会将函数的符号地址设置成真正的映射地址+1实现arm和thumb混编。此外,在切换arm和thumb指令时,还会修改CPSR处理器。在arm中,出了r0-r15这16个处理器,还有状态寄存器CPSR。关于CPSR的其他位这里先不讨论,我们只要知道CPSR寄存器的第低5位T标识了当前的指令集(T=0表示执行arm指令T=1表示执行Thumb指令),所以在切换指令集时需要修改这一位

Arm与Thumb之间的状态切换是通过专用的转移交换指令BX来实现。BX指令以通用寄存器(R0~R15)为操作数,通过拷贝Rn到PC实现绝对跳转。BX利用Rn寄存器中目的地址值的最后一位判断跳转后的状态,如果为“1”表示跳转到Thumb指令集的函数中,如果为“0”表示跳转到Arm指令集的函数中。而Arm指令集的每条指令是32位,即4个字节,也就是说Arm指令的地址肯定是4的倍数,最后两位必定为“00”。所以,直接就可以将从符号表中获得的调用地址模4,看是否为0来判断要修改的函数是用Arm指令集还是Thumb指令集。

三、注入工程Poison-master代码的注入测试

用到的命令:

cd xxxxxAndroidProject_Poison-masteruse_poison

adb push poison /data/local/tmp
adb push libmobisec.so /data/local/tmp
adb shell chmod 0777 /data/local/tmp/poison
adb shell chmod 0777 /data/local/tmp/libmobisec.so
adb shell 
su
ps | grep com.example.androiddecod

cat /proc/17569/maps

/data/local/tmp/poison /data/local/tmp/libmobisec.so 17569

cat /proc/17569/maps | grep libmobisec.so

adb logcat -s TTT

注入so库文件libmobisec.so到com.example.androiddecod进程中成功



能编译成功的项目工程下载地址:http://download.csdn.net/detail/qq1084283172/9721443


四、注入工程Poison-master代码的详细注释说明

整个Poison-master工程的代码的结构图:



整个Poison-master工程代码的详细分析注释:

主文件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;
	// 注入到目标进程中so文件的路径
	char 		*dso;
//	void		*dlopen_addr;
//	void 		*dlsym_addr;
//	void		*mmap_addr;
} process_hook = {0, "", NULL, NULL, NULL};


// 主函数
int main(int argc, char* argv[]) {

	// 对传入的参数的个数进行判断(要求3个参数)
	if(argc < 2)
		exit(0);

	// 保存寄存器的状态信息
	struct pt_regs regs;

	// 获取注入到目标进程中的so的文件的路径
	process_hook.dso = strdup(argv[1]);
	// 获取注入的目标进程的pid
	process_hook.pid = atoi(argv[2]);

//	process_hook.dlopen_addr = (void *)atol(argv[3]);
//	process_hook.dlsym_addr = (void *)atol(argv[4]);
//	process_hook.mmap_addr = (void *)atol(argv[5]);

	// 判断注入到目标进程中so是否存在并且具有可读可执行权限
	if (access(process_hook.dso, R_OK|X_OK) < 0) {

		LOGE("[-] so file must chmod rx
");
		return 1;
	}

	// 获取指定pid进程的名称
	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
", process_hook.pid, get_process_name(process_hook.pid));

	// 读取此时目标进程中所有的寄存器的状态信息
	if (ptrace_getregs(process_hook.pid, &regs) < 0) {

		LOGE("[-] Can't get regs %d
", errno);

		// 读取失败跳转
		goto DETACH;
	}

	// 打印目标进程的寄存器pc和R7的信息
	LOGI("[+] pc: %x, r7: %d", regs.ARM_pc, regs.ARM_r7);

	// dlsym参数为当前进程中的调用地址,获取目标pid进程中dlsy函数的调用地址
	void* remote_dlsym_addr = get_remote_address(process_hook.pid, (void *)dlsym);
	// 获取目标pid进程中dlopen函数的调用地址
	void* remote_dlopen_addr =  get_remote_address(process_hook.pid, (void *)dlopen);

//	if(remote_dlopen_addr == NULL && remote_dlsym_addr != NULL){
//		remote_dlopen_addr = (void *)((uint32_t)remote_dlsym_addr - (uint32_t)process_hook.dlsym_addr + (uint32_t)process_hook.dlopen_addr);
//	}else if(remote_dlopen_addr != NULL && remote_dlsym_addr == NULL){
//		remote_dlsym_addr = (void *)((uint32_t)remote_dlopen_addr - (uint32_t)process_hook.dlopen_addr + (uint32_t)process_hook.dlsym_addr);
//	}else if(remote_dlopen_addr == NULL && remote_dlsym_addr == NULL){
//		LOGE("[-] Can not found dlopen_addr & dlsym_addr.
");
//		goto DETACH;
//	}
//

	// 打印目标进程的函数dlopen和dlsym的调用地址
	LOGI("[+] remote_dlopen address %p
", remote_dlopen_addr);
	LOGI("[+] remote_dlsym  address %p
", remote_dlsym_addr);

	// 调用目标pid进程的dlopen函数加载指定的so库文件,获取返回的加载的模块的基址
	if(ptrace_dlopen(process_hook.pid, remote_dlopen_addr, process_hook.dso) == NULL){

		LOGE("[-] Ptrace dlopen fail. %s
", dlerror());
	}

	// 针对此时不同的模式,设置目标pid进程的CPSR寄存器的值
	if (regs.ARM_pc & 1 ) {
		// thumb
		regs.ARM_pc &= (~1u);
		regs.ARM_cpsr |= CPSR_T_MASK;
	} else {
		// arm
		regs.ARM_cpsr &= ~CPSR_T_MASK;
	}

	// 恢复目标pid进程的寄存器的状态即恢复到注入前的运行状态
	if (ptrace_setregs(process_hook.pid, &regs) == -1) {

		LOGE("[-] Set regs fail. %s
", strerror(errno));
		// 失败进行跳转
		goto DETACH;
	}

	// 打印注入成功的消息
	LOGI("[+] Inject success!
");

DETACH:
    // 结束对目标pid进程的附加
	ptrace_detach(process_hook.pid);

	// 打印注入工作完成的消息
	LOGI("[+] Inject done!
");

	return 0;
}
ptrace_utils.h文件

/*
 * ptrace_utils.h
 *
 *  Created on: 2013-6-19
 *      Author: boyliang
 */

#ifndef PTRACE_UTILS_H_
#define PTRACE_UTILS_H_

#define CPSR_T_MASK		( 1u << 5 )

int ptrace_getregs(pid_t pid, struct pt_regs* regs);

int ptrace_setregs(pid_t pid, struct pt_regs* regs);

int ptrace_attach( pid_t pid , int zygote);

int ptrace_detach( pid_t pid );

int ptrace_continue(pid_t pid);

int ptrace_syscall(pid_t pid);

int ptrace_write(pid_t pid, uint8_t *dest, uint8_t *data, size_t size);

int ptrace_read( pid_t pid,  uint8_t *src, uint8_t *buf, size_t size );

int ptrace_call(pid_t pid, uint32_t addr, long *params, int num_params, struct pt_regs* regs);

void* ptrace_dlopen(pid_t target_pid, void* remote_dlopen_addr, const char*  filename);

#endif /* PTRACE_UTILS_H_ */
ptrace_utils.c文件

/*
 * ptrace_utils.c
 *
 *  Created on: 2013-6-26
 *      Author: boyliang
 */

#include <asm/ptrace.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <dlfcn.h>
#include <cutils/sockets.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <pthread.h>

#include "ptrace_utils.h"
#include "log.h"

/**
 * read registers' status
 */
int ptrace_getregs(pid_t pid, struct pt_regs* regs) {

	if (ptrace(PTRACE_GETREGS, pid, NULL, regs) < 0) {

		perror("ptrace_getregs: Can not get register values");
		return -1;
	}

	return 0;
}

/**
 * set registers' status
 */
int ptrace_setregs(pid_t pid, struct pt_regs* regs) {

	if (ptrace(PTRACE_SETREGS, pid, NULL, regs) < 0) {
		perror("ptrace_setregs: Can not set register values");
		return -1;
	}

	return 0;
}

// 解除zygote进程的阻塞状态
static void* connect_to_zygote(void* arg){

	int s, len;
	struct sockaddr_un remote;

	LOGI("[+] wait 2s...");
	// 休眠一下
	sleep(2);

	/***
	 * zygote启动后会进入一个死循环,用来接收AMS的请求连接.
	 * 当没有应用启动时,zygote进程一直处于阻塞状态。
	 * 所以我们后面代码中的第三次wait会无法返回,解决办法也很简单,就是主动发起一个zygote的连接。
	 * 我们看到第二个waitpid后面调用了一个connect_to_zygote函数。
	 */

	// 创建socket套接字
	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {

		// 设置连接的套接字的协议类型
		remote.sun_family = AF_UNIX;
		// 设置连接的套接字的目标
		strcpy(remote.sun_path, "/dev/socket/zygote");
		// 设置传递的参数的字节长度
		len = strlen(remote.sun_path) + sizeof(remote.sun_family);

		LOGI("[+] start to connect zygote socket");
		// 向"/dev/socket/zygote"目标套接字发起连接
		connect(s, (struct sockaddr *) &remote, len);

		LOGI("[+] close socket");
		// 关闭socket套接字
		close(s);
	}

	/***
	 * 这个函数的功能很简单,先发起socket连接,然后再关闭连接。
	 * 看上去没有做什么有用的事情,但是它却非常重要,
	 * 通过连接zygote,它使zygote进程解除了阻塞状态,
	 * 我们才得以注入进zygote进程。
	 * 参考网址:http://zke1ev3n.me/2015/12/02/Android-so%E6%B3%A8%E5%85%A5/
	 */

	return NULL ;
}

/**
 * attach to target process 附加目标进程
 */
int ptrace_attach(pid_t pid, int zygote) {

	if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) {

		LOGE("ptrace_attach");
		return -1;
	}

	waitpid(pid, NULL, WUNTRACED);

	/*
	 * Restarts  the stopped child as for PTRACE_CONT, but arranges for
	 * the child to be stopped at the next entry to or exit from a sys‐
	 * tem  call,  or  after execution of a single instruction, respec‐
	 * tively.
	 */
	if (ptrace(PTRACE_SYSCALL, pid, NULL, 0) < 0) {

		LOGE("ptrace_syscall");
		return -1;
	}

	waitpid(pid, NULL, WUNTRACED);

	// 针对zygote进程的特殊处理
	if (zygote) {

		// 当进程为zygote时,需要考虑为zygote进程解除阻塞状态,使进程注入得以进行
		connect_to_zygote(NULL);
	}

	// 当目标进程在下次进/出系统调用时被附加调试
	if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL ) < 0) {

		LOGE("ptrace_syscall");
		return -1;
	}

	// 等待进程附加操作返回
	waitpid(pid, NULL, WUNTRACED);

	return 0;
}

/**
 * detach from target process
 */
int ptrace_detach( pid_t pid )
{
    if ( ptrace( PTRACE_DETACH, pid, NULL, 0 ) < 0 )
    {
    	LOGE( "ptrace_detach" );
        return -1;
    }

    return 0;
}


int ptrace_continue(pid_t pid) {

	if (ptrace(PTRACE_CONT, pid, NULL, 0) < 0) {

		LOGE("ptrace_cont");
		return -1;
	}

	return 0;
}

int ptrace_syscall(pid_t pid) {

	return ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
}

/**
 * write data to dest 向目标pid进程中写入数据(4字节对齐)
 */
int ptrace_write(pid_t pid, uint8_t *dest, uint8_t *data, size_t size) {

	uint32_t i, j, remain;
	uint8_t *laddr;

	union u {
		long val;
		char chars[sizeof(long)];
	} d;

	j = size / 4;
	remain = size % 4;

	laddr = data;

	for (i = 0; i < j; i++) {

		memcpy(d.chars, laddr, 4);
		ptrace(PTRACE_POKETEXT, pid, (void *)dest, (void *)d.val);

		dest += 4;
		laddr += 4;
	}

	if (remain > 0) {

		d.val = ptrace(PTRACE_PEEKTEXT, pid, (void *)dest, NULL);
		for (i = 0; i < remain; i++) {

			d.chars[i] = *laddr++;
		}

		ptrace(PTRACE_POKETEXT, pid, (void *)dest, (void *)d.val);

	}

	return 0;
}

// 从目标pid进程中读取数据(4字节对齐)
int ptrace_read( pid_t pid,  uint8_t *src, uint8_t *buf, size_t size )
{
    uint32_t i, j, remain;
    uint8_t *laddr;

    union u {
        long val;
        char chars[sizeof(long)];
    } d;

    j = size / 4;
    remain = size % 4;

    laddr = buf;

    for ( i = 0; i < j; i ++ )
    {
        d.val = ptrace( PTRACE_PEEKTEXT, pid, src, 0 );
        memcpy( laddr, d.chars, 4 );
        src += 4;
        laddr += 4;
    }

    if ( remain > 0 )
    {
        d.val = ptrace( PTRACE_PEEKTEXT, pid, src, 0 );
        memcpy( laddr, d.chars, remain );
    }

    return 0;
}

// 调用目标pid进程中的指定函数addr
int ptrace_call(pid_t pid, uint32_t addr, long *params, int num_params, struct pt_regs* regs) {

	uint32_t i;

	// 在arm中,函数的前4个参数使用r0-r4的寄存器传递
	for (i = 0; i < num_params && i < 4; i++) {

		// 设置调用目标pid进程中的函数需要的参数
		regs->uregs[i] = params[i];
	}

	// 当被调用的函数的参数个数超过4个时,其他的参数通过栈进行传递
	if (i < num_params) {

		// 抬高函数的栈顶
		regs->ARM_sp-= (num_params - i) * sizeof(long);

		// 向目标pid进程的内存中写入函数调用需要的超过4个的其他参数
		ptrace_write(pid, (uint8_t *) regs->ARM_sp, (uint8_t *) ¶ms[i], (num_params - i) * sizeof(long));
	}

	// 设置目标pid进程的pc为将被调用的函数的地址
	regs->ARM_pc= addr;

	// 针对当前进程所处的不同模式,进行不同的处理
	if (regs->ARM_pc& 1) {

		/* thumb模式 */
		regs->ARM_pc &= (~1u);
		regs->ARM_cpsr |= CPSR_T_MASK;

	} else {

		/* arm模式 */
		regs->ARM_cpsr &= ~CPSR_T_MASK;
	}

	// 设置函数调用的返回地址为0,调用的函数执行完,跳回到当前进程中
	regs->ARM_lr= 0;

	// 设置目标pid进程的寄存器的状态,并调用addr函数
	if (ptrace_setregs(pid, regs) == -1 || ptrace_continue(pid) == -1) {

		return -1;
	}

	// 等待函数的调用完成
	waitpid(pid, NULL, WUNTRACED);

	return 0;
}

//static void* thread_connect_to_zygote(void* arg){
//	int s, len;
//	struct sockaddr_un remote;
//
//	LOGI("[+] wait 2s...");
//	sleep(2);
//
//	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {
//		remote.sun_family = AF_UNIX;
//		strcpy(remote.sun_path, "/dev/socket/zygote");
//		len = strlen(remote.sun_path) + sizeof(remote.sun_family);
//		LOGI("[+] start to connect zygote socket");
//		connect(s, (struct sockaddr *) &remote, len);
//		LOGI("[+] close socket");
//		close(s);
//	}
//
//	return NULL ;
//}


// 当目标pid进程为zygote时,加载so库文件之前,需要的测试处理
static int zygote_special_process(pid_t target_pid){

	LOGI("[+] zygote process should special take care. 
");

	struct pt_regs regs;

	// 获取目标pid进程的寄存器的状态值
	if (ptrace_getregs(target_pid, &regs) == -1)
		return -1;

	// 获取目标pid进程的getpid函数的调用地址
	void* remote_getpid_addr = get_remote_address(target_pid, getpid);
	LOGI("[+] Remote getpid addr %p.
", remote_getpid_addr);

	// 判断获取目标pid进程的getpid函数的调用地址是否成功
	if(remote_getpid_addr == NULL){

		return -1;
	}

	pthread_t tid = 0;
	// 创建线程再次调用connect_to_zygote解除zygote进程的阻塞状态
	pthread_create(&tid, NULL, connect_to_zygote, NULL);
	// 释放线程
	pthread_detach(tid);

	// 调用目标pid进程中的getpid函数
	if (ptrace_call(target_pid, remote_getpid_addr, NULL, 0, &regs) == -1) {

		LOGE("[-] Call remote getpid fails");
		return -1;
	}

	// 获取上面的函数调用完后目标pid进程的寄存器的状态,主要是为了获取getpid函数的返回值
	if (ptrace_getregs(target_pid, &regs) == -1)
		return -1;

	// 打印调用getpid函数完后,目标pid进程的寄存器的状态
	LOGI("[+] Call remote getpid result r0=%x, r7=%x, pc=%x, 
", regs.ARM_r0, regs.ARM_r7, regs.ARM_pc);

	return 0;
}

// 调用目标pid进程的dlopen函数加载指定的so库文件,并返回加载的模块的基址
void* ptrace_dlopen(pid_t target_pid, void* remote_dlopen_addr, const char*  filename){

	struct pt_regs regs;

	// 获取目标pid进程的寄存器的状态值
	if (ptrace_getregs(target_pid, &regs) == -1)
		return NULL ;

	// 判断目标pid进程是否是zygote进程;如果是,加载so库文件之前,进行相应的测试处理
	if (strcmp("zygote", get_process_name(target_pid)) == 0 && zygote_special_process(target_pid) != 0) {

		return NULL ;
	}

	// 在目标pid进程中调用dlopen函数需要的参数
	long mmap_params[2];

	// filename为将要加载到目标pid进程中的so的路径字符串
	// 要将filename字符串写入到目标pid进程中,filename_len即为需要分配的内存空间的大小
	size_t filename_len = strlen(filename) + 1;

	// 调用目标pid进程的mmap函数申请内存空间,用以保存filename字符串(即将要加载的so文件的路径)
	void* filename_addr = find_space_by_mmap(target_pid, filename_len);
	// 判断在目标pid进程是否调用mmap函数分配内存空间成功
	if (filename_addr == NULL ) {

		LOGE("[-] Call Remote mmap fails.
");
		return NULL ;
	}

	// 将filename字符串(即将要加载的so文件的路径)写入到目标pid进程的内存地址filename_addr中
	ptrace_write(target_pid, (uint8_t *)filename_addr, (uint8_t *)filename, filename_len);

	// dlopen函数的参数--需要加载的so文件的路径字符串
	mmap_params[0] = (long)filename_addr;
	// dlopen函数的参数--flag,加载的要求
	mmap_params[1] = RTLD_NOW | RTLD_GLOBAL;

	// 获取目标pid进程中的dlopen函数的调用地址(调用参数已经准备好)
	remote_dlopen_addr = (remote_dlopen_addr == NULL) ? get_remote_address(target_pid, (void *)dlopen) : remote_dlopen_addr;
	if (remote_dlopen_addr == NULL) {

		LOGE("[-] Get Remote dlopen address fails.
");
		return NULL;
	}

	// 在目标pid进程调用dlopen函数,加载filename_addr指定的so库文件
	if (ptrace_call(target_pid, (uint32_t) remote_dlopen_addr, mmap_params, 2, &regs) == -1)
		return NULL;

	// 获取目标pid进程的寄存器的状态值,主要是为了获取上面 dlopen函数调用的返回值
	if (ptrace_getregs(target_pid, &regs) == -1)
		return NULL;

	LOGI("[+] Target process returned from dlopen, return r0=%x, r7=%x, pc=%x, 
", regs.ARM_r0, regs.ARM_r7, regs.ARM_pc);

	// 返回目标pid进程中调用dlopen函数的返回的内存加载的模块基址
	return regs.ARM_pc == 0 ? (void *) regs.ARM_r0 : NULL;
}
tool.h文件

/*
 * tool.h
 *
 *  Created on: 2013-7-5
 *      Author: boyliang
 */

#ifndef TOOL_H_
#define TOOL_H_

#include <stdio.h>
#include <dlfcn.h>

// 获取指定内存加载模块的导出函数的地址
void *get_method_address(const char *soname, const char *methodname);

// 获取目标pid进程的名称字符串
const char* get_process_name(pid_t pid);

#endif /* TOOL_H_ */
tool.c文件

/*
 * tool.c
 *
 *  Created on: 2013-7-5
 *      Author: boyliang
 */


#include <stdio.h>
#include <dlfcn.h>
#include <stddef.h>

// 获取指定内存加载模块的导出函数的地址
void *get_method_address(const char *soname, const char *methodname) {

	void *handler = dlopen(soname, RTLD_NOW | RTLD_GLOBAL);

	return dlsym(handler, methodname);
}


// 获取目标pid进程的名称字符串
const char* get_process_name(pid_t pid) {

	static char buffer[255];
	FILE* f;
	char path[255];

	// 格式化得到字符串"/proc/pid/cmdline"
	snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);

	// 读取文件"/proc/pid/cmdline"的内容,获取进程的命令行参数
	if ((f = fopen(path, "r")) == NULL) {

		return NULL;
	}

	// 读取文件"/proc/pid/cmdline"的第1行字符串内容--进程的名称
	if (fgets(buffer, sizeof(buffer), f) == NULL) {

		return NULL;
	}

	// 关闭文件
	fclose(f);

	return buffer;
}
log.h文件

/*
 * log.h
 *
 *  Created on: 2013-6-25
 *      Author: boyliang
 */

#ifndef LOG_H_
#define LOG_H_

#include <android/log.h>

// 主要用于消息的log打印

#define LOG_TAG "TTT"

#ifdef DEBUG
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#else
#define LOGI(...) while(0)
#define LOGE(...) while(0)
#endif

#endif /* LOG_H_ */
elf_utils.h文件

/*
 * elf_utils.h
 *
 *  Created on: 2013-6-19
 *      Author: boyliang
 */

#ifndef ELF_UTILS_H_
#define ELF_UTILS_H_

#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/mman.h>

// 获取目标pid进程中指定so模块的加载基址
void* get_module_base(pid_t pid, const char* module_name);

// 在目标pid进程的内存空间中申请内存,申请成功返回的内存地址保存在r0中
void* find_space_by_mmap(int target_pid, int size);

// 在目标pid进程的"/system/lib/libc.so"的内存范围内(从内存结束地址往回的方向)查找内存空间
void* find_space_in_maps(int pid, int size);

// 通过系统函数的地址查找到该函数所在的模块的名称
int find_module_info_by_address(pid_t pid, void* addr, char *module, void** start, void** end);

// 通过指定的内存模块so的路径字符串,获取该内存模块的在目标进程pid中起始地址和结束地址
int find_module_info_by_name(pid_t pid, const char *module, void** start, void** end);

// 获取目标pid进程中指定函数的调用地址
void* get_remote_address(pid_t pid, void *local_addr);

#endif /* ELF_UTILS_H_ */
elf_utils.c文件

/*
 * elf_utils.c
 *
 *  Created on: 2013-6-25
 *      Author: boyliang
 */


#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ptrace.h>

#include "tools.h"
#include "elf_utils.h"
#include "log.h"


// 获取目标pid进程中指定so模块的加载基址
void* get_module_base(pid_t pid, const char* module_name) {

	FILE *fp;
	long addr = 0;
	char *pch;
	char filename[32];
	char line[1024];

	if (pid < 0) {

		/* self process */
		snprintf(filename, sizeof(filename), "/proc/self/maps");
	} else {

		snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
	}

	fp = fopen(filename, "r");

	if (fp != NULL) {

		while (fgets(line, sizeof(line), fp)) {

			// 判断是否是在目标pid进程的内存中要查找到的so模块
			if (strstr(line, module_name)) {

				pch = strtok(line, "-");
				// 获取目标pid进程中指定模块的基址
				addr = strtoul(pch, NULL, 16);

				if (addr == 0x8000)
					addr = 0;

				break;
			}
		}

		fclose(fp);
	}

	return (void *) addr;
}

// 在目标pid进程的内存空间中申请内存,申请成功返回的内存地址保存在r0中
void* find_space_by_mmap(int target_pid, int size) {

	struct pt_regs regs;

	// 获取目标pid进程的寄存器的状态
	if (ptrace_getregs(target_pid, &regs) == -1)
		return 0;

	long parameters[10];

	/* call mmap */
	parameters[0] = 0;  // addr
	parameters[1] = size; // size
	parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC;  // prot
	parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flags
	parameters[4] = 0; //fd
	parameters[5] = 0; //offset

	// 获取目标pid进程中的mmap函数的调用地址
	void *remote_mmap_addr = get_remote_address(target_pid, get_method_address("/system/lib/libc.so", "mmap"));
	LOGI("[+] Calling mmap in target process. mmap addr %p.
", remote_mmap_addr);

	if (remote_mmap_addr == NULL) {

		LOGE("[-] Get Remote mmap address fails.
");
		return 0;
	}

	// 调用目标pid进程的mmap函数,在目标pid进程的内存中申请内存空间
	if (ptrace_call(target_pid, (uint32_t) remote_mmap_addr, parameters, 6, &regs) == -1)
		return 0;

	// 获取目标pid进程的寄存器的状态
	if (ptrace_getregs(target_pid, &regs) == -1)
		return 0;

	LOGI("[+] Target process returned from mmap, return r0=%x, r7=%x, pc=%x, 
", regs.ARM_r0, regs.ARM_r7, regs.ARM_pc);

	// arm中,函数的返回值保存在寄存器r0中,返回在目标pid进程中申请的内存空间的地址
	return regs.ARM_pc == 0 ? (void *) regs.ARM_r0 : 0;
}

// 分割字符串
static char* nexttok(char **strp) {

	// 以" "为基准分解字符串,将原字符串中第一个" "替换为''
	// 第一个" "前面的字符串返回在p中,第一个" "后面的字符串在strp中
	char *p = strsep(strp, " ");

	// 返回分割的字符串
	return p == NULL ? "" : p;
}

// 在目标pid进程的"/system/lib/libc.so"的内存范围内(从内存结束地址往回的方向)查找内存空间
void* find_space_in_maps(int pid, int size) {

	char statline[1024];
	FILE * fp;
	uint32_t* addr = (uint32_t*) 0x40008000;
	char *address, *proms, *ptr;
	const char* tname = "/system/lib/libc.so";
	const char* tproms = "r-xp";

	// 获取字符串"/system/lib/libc.so"的长度
	int tnaem_size = strlen(tname);
	// 获取字符串"r-xp"的长度
	int tproms_size = strlen(tproms);

	// 内存以4字节对齐
	size = ((size / 4) + 1) * 4;

	// 格式化得到字符串"/proc/pid/maps"
	sprintf(statline, "/proc/%d/maps", pid);

	// 打开文件"/proc/pid/maps"
	fp = fopen(statline, "r");
	if (fp == 0)
		return 0;

	// 读取文件"/proc/pid/maps"中内容(每次读一行)
	while (fgets(statline, sizeof(statline), fp)) {

		// 分割字符串
		ptr = statline;
		// 得到内存模块的起始和结束地址
		address = nexttok(&ptr); // skip address
		// 内存模块的属性
		proms = nexttok(&ptr); // skip proms
		nexttok(&ptr); // skip offset
		nexttok(&ptr); // skip dev
		nexttok(&ptr); // skip inode

		// ptr中最终保存的是加载的内存模块的路径字符串
		while (*ptr != '') {
			if (*ptr == ' ')
				ptr++;
			else
				break;
		}

		// 查找目标so模块
		if (ptr && proms && address) {

			// 判断是否是"r-xp"属性的模块
			if (strncmp(tproms, proms, tproms_size) == 0) {

				// 判断是否是"/system/lib/libc.so"模块
				if (strncmp(tname, ptr, tnaem_size) == 0) {

					// address like afe00000-afe3a000
					if (strlen(address) == 17) {

						// 获取内存加载模块/system/lib/libc.so的内存范围的结束地址(方便后面查找内存空间)
						addr = (uint32_t*) strtoul(address + 9, NULL, 16);
						// 在目标pid进程的/system/lib/libc.so的内存范围内查找到size大小内存空间
						addr -= size;

						printf("proms=%s address=%s name=%s", proms, address, ptr);
						break;
					}
				}
			}
		}
	}

	// 关闭文件
	fclose(fp);

	// 返回在目标进程中查找到的内存空间的地址
	return (void*) addr;
}


// 通过系统函数的地址查找到该函数所在的模块的名称
int find_module_info_by_address(pid_t pid, void* addr, char *module, void** start, void** end) {

	char statline[1024];
	FILE *fp;
	char *address, *proms, *ptr, *p;

	// 格式化字符串得到"/proc/pid/maps"
	if ( pid < 0 ) {

		/* self process */
		snprintf( statline, sizeof(statline), "/proc/self/maps");
	} else {

		snprintf( statline, sizeof(statline), "/proc/%d/maps", pid );
	}

	// 打开文件 /proc/pid/maps
	fp = fopen( statline, "r" );
	if ( fp != NULL ) {

		// 每次一行,读取文件/proc/pid/maps中内容
		while ( fgets( statline, sizeof(statline), fp ) ) {

			// 解析读取为一行字符串信息
			ptr = statline;
			// 获取模块的起始和结束地址
			address = nexttok(&ptr); // skip address
			proms = nexttok(&ptr); // skip proms
			nexttok(&ptr); // skip offset
			nexttok(&ptr); // skip dev
			nexttok(&ptr); // skip inode

			while(*ptr != '') {
				if(*ptr == ' ')
					ptr++;
				else
					break;
			}

			p = ptr;
			while(*p != '') {
				if(*p == '
')
					*p = '';
				p++;
			}

			// 4016a000-4016b000
			if(strlen(address) == 17) {

				address[8] = '';

				// 获取内存加载模块的起始地址
				*start = (void*)strtoul(address, NULL, 16);
				// 获取内存加载模块的结束地址
				*end   = (void*)strtoul(address+9, NULL, 16);

				// printf("[%p-%p] %s | %p
", *start, *end, ptr, addr);

				// 判断该系统函数的地址是否在该模块的内存范围内
				if(addr > *start && addr < *end) {

					// 找到该系统函数所在的内存模块
					// 保存该内存加载的so模块的文件路径
					strcpy(module, ptr);

					fclose( fp );
					return 0;
				}
			}
		}

		fclose( fp ) ;
	}

	return -1;
}

// 通过指定的内存模块so的路径字符串,获取该内存模块的在目标进程pid中起始地址和结束地址
int find_module_info_by_name(pid_t pid, const char *module, void** start, void** end) {

	char statline[1024];
	FILE *fp;
	char *address, *proms, *ptr, *p;

	if ( pid < 0 ) {

		/* self process */
		snprintf( statline, sizeof(statline), "/proc/self/maps");
	} else {

		snprintf( statline, sizeof(statline), "/proc/%d/maps", pid );
	}

	fp = fopen( statline, "r" );

	if ( fp != NULL ) {

		while ( fgets( statline, sizeof(statline), fp ) ) {

			ptr = statline;
			address = nexttok(&ptr); // skip address
			proms = nexttok(&ptr); // skip proms
			nexttok(&ptr); // skip offset
			nexttok(&ptr); // skip dev
			nexttok(&ptr); // skip inode

			while(*ptr != '') {

				if(*ptr == ' ')
					ptr++;
				else
					break;
			}

			p = ptr;
			while(*p != '') {

				if(*p == '
')
					*p = '';
				p++;
			}

			// 4016a000-4016b000
			if(strlen(address) == 17) {

				address[8] = '';

				*start = (void*)strtoul(address, NULL, 16);
				*end   = (void*)strtoul(address+9, NULL, 16);

				// printf("[%p-%p] %s | %p
", *start, *end, ptr, addr);

				// 通过内存模块的路径字符串,判读是否是要查找的目标内存so模块
				if(strncmp(module, ptr, strlen(module)) == 0) {

					fclose( fp ) ;

					return 0;
				}
			}
		}

		fclose( fp ) ;
	}

	return -1;
}


// 获取目标pid进程中指定函数的调用地址
void* get_remote_address(pid_t pid, void *local_addr) {

	// 保存加载的内存so模块的文件路径字符串
	char buf[256];
	// 当前进程中指定模块的起始地址
	void* local_start = 0;
	// 当前进程中指定模块的结束地址
	void* local_end = 0;
	// 目标pid进程中指定模块的起始地址
	void* remote_start = 0;
	// 目标pid进程中指定模块的结束地址
	void* remote_end = 0;

	// 获取当前进程中指定系统函数所在的模块的文件路径字符串buf
	if(find_module_info_by_address(-1, local_addr, buf, &local_start, &local_end) < 0) {

		LOGI("[-] find_module_info_by_address FAIL");
		return NULL;
	}

	LOGI("[+] the local module is %s", buf);

	// 通过指定的内存模块so的路径字符串,获取该内存模块的在目标进程pid中起始地址和结束地址
	if(find_module_info_by_name(pid, buf, &remote_start, &remote_end) < 0) {


		LOGI("[-] find_module_info_by_name FAIL");
		return NULL;
	}

	// 目标pid进程的local_addr函数的调用地址
	return (void *)( (uint32_t)local_addr + (uint32_t)remote_start - (uint32_t)local_start );
}
Android.mk文件

LOCAL_PATH := $(call my-dir)  
  
include $(CLEAR_VARS)  

# 编译生成的模块的名称
LOCAL_MODULE := poison  

# 需要被编译的源码文件 
LOCAL_SRC_FILES := poison.c 
	       elf_utils.c 
	       ptrace_utils.c 
	       tools.c 

# 支持log日志打印android/log.h里函数调用的需要
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog  

# 编译模块生成可执行文件 
include $(BUILD_EXECUTABLE)  
Application.mk文件

# 编译生成的模块运行支持的平台
APP_ABI := armeabi-v7a  
# 设置编译连接的工具的版本
#NDK_TOOLCHAIN_VERSION = 4.9  


感谢链接

https://github.com/matrixhawk/Poison

https://github.com/boyliang/ndk-patch

http://zke1ev3n.me/2015/12/02/Android-so注入/

http://blog.csdn.net/qq1084283172/article/details/46859931

http://www.cnblogs.com/leaven/archive/2011/01/25/1944688.html

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

http://blog.csdn.net/jinzhuojun/article/details/9900105



原文地址:https://www.cnblogs.com/csnd/p/11800652.html