通过JNI的方式(NDK编程),fork()出一个子线程作为守护进程,轮询监听服务状态。守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。而守护进程的会话组和当前目录,文件描述符都是独立的。后台运行只是终端进行了一次fork,让程序在后台执行,这些都没有改变。
那么我们先来看看Android4.4的源码,ActivityManagerService(源码/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java)是如何关闭在应用退出后清理内存的:
1. Process.killProcessQuiet(pid);
应用退出后,ActivityManagerService就把主进程给杀死了,但是,在 Android5.0 中,ActivityManagerService却是这样处理的:
1. Process.killProcessQuiet(app.pid);
2. Process.killProcessGroup(app.info.uid, app.pid);
就差了一句话,却差别很大。 Android5.0在应用退出后,ActivityManagerService不仅把主进程给杀死,另外把主进程所属的进程组一并杀死,这样一来,由于子进程和主进程在同一进程组,子进程在做的事情,也就停止了 ...要不怎么说Android5.0在安全方面做了很多更新呢...
那么,有没有办法让子进程脱离出来,不要受到主进程的影响,当然也是可以的。那么,在C/C++层是如何实现的呢?先上关键代码:
1. /**
2. * srvname 进程名
3. * sd 之前创建子进程的pid写入的文件路径
4. */
5. int start(int argc, char* srvname, char* sd) {
6. pthread_t id;
7. int ret;
8. struct rlimit r;
9.
10. int pid = fork();
11. "fork pid: %d", pid);
12. if (pid < 0) {
13. "first fork() error pid %d,so exit", pid);
14. exit(0);
15. else if (pid != 0) {
16. "first fork(): I'am father pid=%d", getpid());
17. //exit(0);
18. else { // 第一个子进程
19. "first fork(): I'am child pid=%d", getpid());
20. setsid();
21. "first fork(): setsid=%d", setsid());
22. //为文件赋予更多的权限,因为继承来的文件可能某些权限被屏蔽
23.
24. int pid = fork();
25. if (pid == 0) { // 第二个子进程
26. FILE *fp;
27. "%s/pid",sd);
28. if((fp=fopen(sd,"a"))==NULL) {//打开文件 没有就创建
29. "%s文件还未创建!",sd);
30. ftruncate(fp, 0);
31. lseek(fp, 0, SEEK_SET);
32. }
33. fclose(fp);
34. "rw");
35. if(fp>0){
36. char buff1[6];
37. int p = 0;
38. sizeof(buff1));
39. fseek(fp,0,SEEK_SET);
40. //读取一行(pid)
41. "读取的进程号:%s",buff1);
42. if(strlen(buff1)>1){ // 有值
43. // 把上一次的进程干掉,防止重复执行
44. "杀死进程,pid=%d",atoi(buff1));
45. }
46. }
47. fclose(fp);
48. "w");
49. char buff[100];
50. int k = 3;
51. if(fp>0){
52. "%lu",getpid());
53. "%s\n",buff); // 把进程号写入文件
54. }
55. fclose(fp);
56. fflush(fp);
57.
58. "I'am child-child pid=%d", getpid());
59. "/"); //<span style="font-family: Arial, Helvetica, sans-serif;">修改进程工作目录为根目录,chdir(“/”)</span>
60. //关闭不需要的从父进程继承过来的文件描述符。
61. if (r.rlim_max == RLIM_INFINITY) {
62. r.rlim_max = 1024;
63. }
64. int i;
65. for (i = 0; i < r.rlim_max; i++) {
66. close(i);
67. }
68.
69. umask(0);
70. void *) thread, srvname); // 开启线程,轮询去监听启动服务
71. if (ret != 0) {
72. "Create pthread error!\n");
73. exit(1);
74. }
75. int stdfd = open ("/dev/null", O_RDWR);
76. dup2(stdfd, STDOUT_FILENO);
77. dup2(stdfd, STDERR_FILENO);
78. else {
79. exit(0);
80. }
81. }
82. return 0;
83. }
84.
85. /**
86. * 启动Service
87. */
88. void Java_com_yyh_fork_NativeRuntime_startService(JNIEnv* env, jobject thiz,
89. jstring cchrptr_ProcessName, jstring sdpath) {
90. char * rtn = jstringTostring(env, cchrptr_ProcessName); // 得到进程名称
91. char * sd = jstringTostring(env, sdpath);
92. "Java_com_yyh_fork_NativeRuntime_startService run....ProcessName:%s", rtn);
93. a = rtn;
94. start(1, rtn, sd);
95. }
这里有几个重点需要理解一下:
1、为什么要fork两次?
第一次fork的作用是为后面setsid服务。setsid的调用者不能是进程组组长(group leader),而第一次调用的时候父进程是进程组组长。
第二次调用后,把前面一次fork出来的子进程退出,这样第二次fork出来的子进程,就和他们脱离了关系。
2、setsid()作用是什么?setsid() 使得第二个子进程是会话组长(sid==pid),也是进程组组长(pgid == pid),并且脱离了原来控制终端。故不管控制终端怎么操作,新的进程正常情况下不会收到他发出来的这些信号。
3、umask(0)的作用:由于子进程从父进程继承下来的一些东西,可能并未把权限继承下来,所以要赋予他更高的权限,便于子进程操作。
4、chdir ("/");作用:进程活动时,其工作目录所在的文件系统不能卸下,一般需要将工作目录改变到根目录。
5、进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。所以在最后,记得关闭掉从父进程继承过来的文件描述符。
然后,在上面的代码中开启线程后做的事,就是循环去startService(),代码如下:
1. void thread(char* srvname) {
2. while(1){
3. check_and_restart_service(srvname);
4. sleep(4);
5. }
6. }
7.
8. /**
9. * 检测服务,如果不存在服务则启动.
10. * 通过am命令启动一个laucher服务,由laucher服务负责进行主服务的检测,laucher服务在检测后自动退出
11. */
12. void check_and_restart_service(char* service) {
13. "当前所在的进程pid=",getpid());
14. char cmdline[200];
15. "am startservice --user 0 -n %s", service);
16. char tmp[200];
17. "cmd=%s", cmdline);
18. ExecuteCommandWithPopen(cmdline, tmp, 200);
19. LOGI( tmp, LOG);
20. }
21.
22. /**
23. * 执行命令
24. */
25. void ExecuteCommandWithPopen(char* command, char* out_result,
26. int resultBufferSize) {
27. FILE * fp;
28. '\0';
29. "r");
30. if (fp) {
31. fgets(out_result, resultBufferSize - 1, fp);
32. '\0';
33. pclose(fp);
34. else {
35. "popen null,so exit");
36. exit(0);
37. }
38. }
这两个启动服务的函数,里面就涉及到一些Android和linux的命令了,这里我就不细说了。特别是am,挺强大的功能的,不仅可以开启服务,也可以开启广播等等...然后调用ndk-build命令进行编译,生成so库,ndk不会的,自行百度咯~
C/C++端关键的部分主要是以上这些,自然而然,Java端还得配合执行。
首先来看一下C/C++代码编译完的so库的加载类,以及native的调用:
1. package com.yyh.fork;
2.
3. import java.io.DataInputStream;
4. import java.io.DataOutputStream;
5. import java.io.File;
6.
7. public class NativeRuntime {
8.
9. private static NativeRuntime theInstance = null;
10.
11. private NativeRuntime() {
12.
13. }
14.
15. public static NativeRuntime getInstance() {
16. if (theInstance == null)
17. new NativeRuntime();
18. return theInstance;
19. }
20.
21. /**
22. * RunExecutable 启动一个可自行的lib*.so文件
23. * @date 2016-1-18 下午8:22:28
24. * @param pacaageName
25. * @param filename
26. * @param alias 别名
27. * @param args 参数
28. * @return
29. */
30. public String RunExecutable(String pacaageName, String filename, String alias, String args) {
31. "/data/data/" + pacaageName;
32. "/lib/" + filename;
33. "/" + alias;
34. "/" + alias + " " + args;
35. "chmod 777 " + cmd2;
36. "dd if=" + cmd1 + " of=" + cmd2;
37. new StringBuffer();
38.
39. if (!new File("/data/data/" + alias).exists()) {
40. // 拷贝lib/libtest.so到上一层目录,同时命名为test.
41. ";");
42. }
43. // 改变test的属性,让其变为可执行
44. ";");
45. // 执行test程序.
46. ";");
47. return sb_result.toString();
48. }
49.
50. /**
51. * 执行本地用户命令
52. * @date 2016-1-18 下午8:23:01
53. * @param pacaageName
54. * @param command
55. * @param sb_out_Result
56. * @return
57. */
58. public boolean RunLocalUserCommand(String pacaageName, String command, StringBuffer sb_out_Result) {
59. null;
60. try {
61. "sh"); // 获得shell进程
62. new DataInputStream(process.getInputStream());
63. new DataOutputStream(process.getOutputStream());
64. "cd /data/data/" + pacaageName + "\n"); // 保证在command在自己的数据目录里执行,才有权限写文件到当前目录
65. " &\n"); // 让程序在后台运行,前台马上返回
66. "exit\n");
67. outputStream.flush();
68. process.waitFor();
69. byte[] buffer = new byte[inputStream.available()];
70. inputStream.read(buffer);
71. new String(buffer);
72. if (sb_out_Result != null)
73. "CMD Result:\n" + s);
74. catch (Exception e) {
75. if (sb_out_Result != null)
76. "Exception:" + e.getMessage());
77. return false;
78. }
79. return true;
80. }
81.
82. public native void startActivity(String compname);
83.
84. public native String stringFromJNI();
85.
86. public native void startService(String srvname, String sdpath);
87.
88. public native int findProcess(String packname);
89.
90. public native int stopService();
91.
92. static {
93. try {
94. "helper"); // 加载so库
95. catch (Exception e) {
96. e.printStackTrace();
97. }
98. }
99.
100. }
然后,我们在收到开机广播后,启动该服务。
1. package com.yyh.activity;
2.
3. import android.content.BroadcastReceiver;
4. import android.content.Context;
5. import android.content.Intent;
6. import android.util.Log;
7.
8. import com.yyh.fork.NativeRuntime;
9. import com.yyh.utils.FileUtils;
10. public class PhoneStatReceiver extends BroadcastReceiver {
11.
12. private String TAG = "tag";
13.
14. @Override
15. public void onReceive(Context context, Intent intent) {
16. if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
17. "手机开机了~~");
18. "/com.yyh.service.HostMonitor", FileUtils.createRootPath());
19. else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {
20. }
21. }
22.
23.
24. }
Service服务里面,就可以做该做的事情。
1. package com.yyh.service;
2.
3. import android.app.Service;
4. import android.content.Intent;
5. import android.os.IBinder;
6. import android.util.Log;
7.
8. public class HostMonitor extends Service {
9.
10. @Override
11. public void onCreate() {
12. super.onCreate();
13. "daemon_java", "HostMonitor: onCreate! I can not be Killed!");
14. }
15.
16. @Override
17. public int onStartCommand(Intent intent, int flags, int startId) {
18. "daemon_java", "HostMonitor: onStartCommand! I can not be Killed!");
19. return super.onStartCommand(intent, flags, startId);
20. }
21.
22. @Override
23. public IBinder onBind(Intent arg0) {
24. return null;
25. }
26. }
当然,也不要忘记在Manifest.xml文件配置receiver和service:
1. <receiver
2. android:name="com.yyh.activity.PhoneStatReceiver"
3. android:enabled="true"
4. android:permission="android.permission.RECEIVE_BOOT_COMPLETED" >
5. <intent-filter>
6. <action android:name="android.intent.action.BOOT_COMPLETED" />
7. <action android:name="android.intent.action.USER_PRESENT" />
8. </intent-filter>
9. </receiver>
10.
11. <service android:name="com.yyh.service.HostMonitor"
12. android:enabled="true"
13. android:exported="true">
14. </service>
run起来,在程序应用里面,结束掉这个进程,不一会了,又自动起来了~~~~完美~~~~跟流氓软件一个样,没错,就是这么贱,就是这么霸道!!
这边是运行在谷歌的原生系统上,Android版本为5.0...不知道会不会被国内的各大ROM干掉,好紧张好紧张~~~所以,总结一下就是:服务常驻要应对的不是各种难的技术,而是各大ROM。QQ为什么不会被杀死,是因为国内各大ROM不想让他死...
最后附上本例的源代码:Android 通过JNI实现双守护进程,保证服务不被杀死 源码
从技术角度概括一下现在普遍的防杀方法
1.Service设置成START_STICKY,kill 后会被重启(等待5秒左右),重传Intent,保持与重启前一样
2.通过 startForeground将进程设置为前台进程,做前台服务,优先级和前台应用一个级别,除非在系统内存非常缺,否则此进程不会被 kill
2.双进程Service:让2个进程互相保护,其中一个Service被清理后,另外没被清理的进程可以立即重启进程
3.QQ黑科技:在应用退到后台后,另起一个只有 1 像素的页面停留在桌面上,让自己保持前台状态,保护自己不被后台清理工具杀死
4.在已经root的设备下,修改相应的权限文件,将App伪装成系统级的应用(Android4.0系列的一个漏洞,已经确认可行)
5.Android系统中当前进程(Process)fork出来的子进程,被系统认为是两个不同的进程。当父进程被杀死的时候,子进程仍然可以存活,并不受影响。鉴于目前提到的在Android-Service层做双守护都会失败,我们可以fork出c进程,多进程守护。死循环在那检查是否还存在,具体的思路如下(Android5.0以下可行):
1.用C编写守护进程(即子进程),守护进程做的事情就是循环检查目标进程是否存在,不存在则启动它。
2.在NDK环境中将1中编写的C代码编译打包成可执行文件(BUILD_EXECUTABLE)。
3.主进程启动时将守护进程放入私有目录下,赋予可执行权限,启动它即可。
6.联系厂商,加入白名单