Android 流氓软件静默安装是怎么实现的?

4,250 阅读6分钟

序言

乍听起来,静默安装是非常流氓的一件事,它让用户不知觉的情况下被「收割」。但是技术本身是中立的,我们只谈谈实现静默安装这件事儿。

下面我将介绍三种静默安装的方案,每种方案各有利弊,但是目的是一致的。

  1. 手机被 Root 后直接静默安装
  2. 声明安装权限并进行系统签名来静默安装
  3. 使用辅助功能进行安装(称作「智能安装」更贴切吧)

1. 手机被 Root 后直接静默安装

众所周知,手机被 Root 后可以做好多奇妙的事情,比如静默安装,直接调用 pm install 命令就可以实现,来看代码:

    public static boolean silentInstall(String apkPath) {
        boolean result = false;
        DataOutputStream dataOutputStream = null;
        BufferedReader errorStream = null;
        BufferedReader successStream = null;
        Process process = null;
        try {
            // 申请 su 权限
            process = Runtime.getRuntime().exec("su");
            dataOutputStream = new DataOutputStream(process.getOutputStream());
            // 执行 pm install 命令
            String command = "pm install -r " + apkPath + "\n";
            dataOutputStream.write(command.getBytes(Charset.forName("UTF-8")));
            dataOutputStream.writeBytes("exit\n");
            dataOutputStream.flush();
            process.waitFor();
            errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            StringBuilder errorMsg = new StringBuilder();
            String line;
            while ((line = errorStream.readLine()) != null) {
                errorMsg.append(line);
            }
            log.debug("silent install error message:{}", errorMsg);
            StringBuilder successMsg = new StringBuilder();
            successStream = new BufferedReader(new InputStreamReader(process.getInputStream()));
            // 读取命令执行结果
            while ((line = successStream.readLine()) != null) {
                successMsg.append(line);
            }
            log.debug("silent install success message:{}", successMsg);
            // 如果执行结果中包含 Failure 字样就认为是操作失败,否则就认为安装成功
            if (!(errorMsg.toString().contains("Failure") || successMsg.toString().contains("Failure"))) {
                result = true;
            }
        } catch (Exception e) {
            log.error(e);
        } finally {
            try {
                if (process != null) {
                    process.destroy();
                }
                if (dataOutputStream != null) {
                    dataOutputStream.close();
                }
                if (errorStream != null) {
                    errorStream.close();
                }
                if (successStream != null) {
                    successStream.close();
                }
            } catch (Exception e) {
                // ignored
            }
        }
        return result;
    }

    public static boolean isRoot() {
        return new File("/system/bin/su").exists() || new File("/system/xbin/su").exists();
    }

首先申请 Root 权限,然后执行 pm install- r <apk 路径>命令,-r 参数表示允许覆盖安装。 process.waitFor() 说明安装过程是同步的,等待命令执行完成,然后读取执行结果。注意:不要在主线程调用静默安装的代码,安装过程会比较耗时,导致线程一直等待结果。

结论:只要手机被 Root,该方法十分奏效。但是绝大部分用户不懂 Root,即使手机被 Root了,还需要用户授权,所以该方案局限性非常大。

2. 声明安装权限并进行系统签名来静默安装

当我们选择手动安装应用时,会跳转到应用安装界面,这个界面就是系统的 PackageInstaller 提供,专门用来让用户有感知地安装应用。

    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri uri = Uri.fromFile(new File("/sdcard/news.apk")));
    intent.setDataAndType(uri, "application/vnd.android.package-archive");
    startActivity(intent);

分析 PackageInstaller 的源码,我们发现它会通过 PackageManager 调用 installPackage 方法,这是个隐藏的抽象方法,实现类是 ApplicationPackageManager。主要看一下四个参数:packageURI 就是 apk 的路径;observer 是安装的监听器,应用安装完成时会被回调,不能为 null;flags 是标志位,指定安装的参数;installersPackageName 表示可选的安装来源,比如应用宝之类的。

    public abstract void installPackage(
            Uri packageURI, PackageInstallObserver observer,
            int flags, String installerPackageName);

ApplicationPackageManager 里面 mPM 是一个 IPackageManager 类型的对象,它会执行具体的安装任务。

        try {
            mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName,
                    verificationParams, null);
        } catch (RemoteException ignored) {
        }

ContextImpl 的 getPackageManager 方法,通过 ActivityThread 获取 IPackageManager 对象用来构造 ApplicationPackageManager,然后返回 ApplicationPackageManager。

    public PackageManager getPackageManager() {
        if (mPackageManager != null) {
            return mPackageManager;
        }

        IPackageManager pm = ActivityThread.getPackageManager();
        if (pm != null) {
            // Doesn't matter if we make more than one instance.
            return (mPackageManager = new ApplicationPackageManager(this, pm));
        }
        return null;
    }

ActivityThread 的 getPackageManager 方法,其实就是获取系统服务的过程。

    public static IPackageManager getPackageManager() {
        if (sPackageManager != null) {
            //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
            return sPackageManager;
        }
        IBinder b = ServiceManager.getService("package");
        //Slog.v("PackageManager", "default service binder = " + b);
        sPackageManager = IPackageManager.Stub.asInterface(b);
        //Slog.v("PackageManager", "default service = " + sPackageManager);
        return sPackageManager;
    }

通过以上分析,我们通过 PackageManager 调用 installPackage 方法就行了,下面看代码:

   public static boolean silentInstall(PackageManager packageManager, String apkPath) {
        Class<?> pmClz = packageManager.getClass();
        try {
            if (Build.VERSION.SDK_INT >= 21) {
                Class<?> aClass = Class.forName("android.app.PackageInstallObserver");
                Constructor<?> constructor = aClass.getDeclaredConstructor();
                constructor.setAccessible(true);
                Object installObserver = constructor.newInstance();
                Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, aClass, int.class, String.class);
                method.setAccessible(true);
                method.invoke(packageManager, Uri.fromFile(new File(apkPath)), installObserver, 2, null);
            } else {
                Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, Class.forName("android.content.pm.IPackageInstallObserver"), int.class, String.class);
                method.setAccessible(true);
                method.invoke(packageManager, Uri.fromFile(new File(apkPath)), null, 2, null);
            }
            return true;
        } catch (Exception e) {
            log.error(e);
        }
        return false;
    }

由于 PackageManager 在不同版本上的 installPackage 方法参数不一致,所以我们根据编译版本做了处理。在 API 21 及以上,需要传递一个非 null 的 PackageInstallObserver,这个类是不可见 的,我们就用反射创建一个 observer 对象,flags 指定 INSTALL_REPLACE_EXISTING,用常量表示就是 2。在 API 21 以下,observer 类型是IPackageInstallObserver,同样使用反射处理即可。

最后声明权限 <uses-permission android:name="android.permission.INSTALL_PACKAGES"/>,还要使用系统签名,这个非常关键,要不然就会出现异常: java.lang.SecurityException: Neither user 10052 nor current process has android.permission.INSTALL_PACKAGES.

结论:通过调用系统 API 静默安装,终于可以堂堂正正地搞事情了!虽然这是官方提供的接口,但是为了不让你为所欲为,强制使用系统签名,所以对于第三方应用采用的可能性是零。

3. 使用辅助功能进行安装

现在大多数应用采取的是这种办法,让用户主动打开辅助功能,然后模拟点击用户操作,进行自动安装。核心就是 AccessibilityService,我们来实现这一功能。

  1. 创建 AccessibilityService 配置文件 在 res 目录下创建 xml 目录,然后在 xml 目录下创建 accessibility_service_config.xml 文件,内容如下
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
                       android:accessibilityEventTypes="typeAllMask"
                       android:accessibilityFeedbackType="feedbackGeneric"
                       android:accessibilityFlags="flagDefault"
                       android:canRetrieveWindowContent="true"
                       android:description="@string/accessibility_service_description"
                       android:packageNames="com.android.packageinstaller"
    />

accessibilityEventTypes: 指定我们在监听窗口中可以模拟哪些事件,typeAllMask 表示所有的事件都能模拟。 accessibilityFeedbackType: 指定无障碍服务的反馈方式。 canRetrieveWindowContent: 指定是否允许我们的程序读取窗口中的节点和内容,当然是 true。 description: 当用户手动配置服务时,显示给用户看的说明信息。 packageNames: 指定要监听哪个应用程序下的窗口活动,这里写 com.android.packageinstaller 表示监听 Android 系统的安装界面。 配置里面描述的内容

<resources>  
    <string name="app_name">InstallTest</string>  
    <string name="accessibility_service_description">智能安装服务,无需用户的任何操作就可以自动安装程序。</string>  
</resources>  
  1. 创建 AccessibilityService 服务 创建一个类继承自 AccessibilityService ,然后重写 onAccessibilityEvent 方法。
public class MyAccessibilityService extends AccessibilityService {
    private static final String TAG = "[TAG]";
    private Map<Integer, Boolean> handleMap = new HashMap<>();

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        AccessibilityNodeInfo nodeInfo = event.getSource();
        if (nodeInfo != null) {
            int eventType = event.getEventType();
            if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                if (handleMap.get(event.getWindowId()) == null) {
                    boolean handled = iterateNodesAndHandle(nodeInfo);
                    if (handled) {
                        handleMap.put(event.getWindowId(), true);
                    }
                }
            }
        }
    }

    @Override
    public void onInterrupt() {
    }
    // 遍历节点,模拟点击安装按钮
    private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            int childCount = nodeInfo.getChildCount();
            if ("android.widget.Button".equals(nodeInfo.getClassName())) {
                String nodeCotent = nodeInfo.getText().toString();
                Log.d(TAG, "content is: " + nodeCotent);
                if ("安装".equals(nodeCotent) || "完成".equals(nodeCotent) || "确定".equals(nodeCotent)) {
                    nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    return true;
                }
            }
            // 遇到 ScrollView 的时候模拟滑动一下
            else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
            }
            for (int i = 0; i < childCount; i++) {
                AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i);
                if (iterateNodesAndHandle(childNodeInfo)) {
                    return true;
                }
            }
        }
        return false;
    }
}

当进入安装界面就会回调 onAccessibilityEvent() 这个方法,我们只处理 TYPE_WINDOW_CONTENT_CHANGED 和 TYPE_WINDOW_STATE_CHANGED 两个事件。为了防止重复处理事件,用 map 来过滤事件,然后递归遍历节点,找到「安装」、「完成」、「缺点」的按钮就模拟点击。由于安装界面需要用户看完权限才出现按钮,所以遇到 ScrollView 的时候就模拟滚动,直到出现安装按钮。

  1. 在 AndroidManifest 中配置服务
        <service
            android:name=".MyAccessibilityService"
            android:label="智能安装应用"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            >
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService"/>
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config"
                />
        </service>

android:label:这个就是用户看到的无障碍服务的名称。 android:permission: 需要用到 BIND_ACCESSIBILITY_SERVICE 这个权限。 action: android.accessibilityservice.AccessibilityService 有了这个 action,用户才能在设置里面看到我们的服务,否则用户无法开启我们的 AccessibilityService,也就不能进到 MyAccessibilityService 里面了。

  1. 调用智能安装代码
    private void smartInstall() {
        Uri uri = Uri.fromFile(new File("/sdcard/test.apk"));
        Intent localIntent = new Intent(Intent.ACTION_VIEW);
        localIntent.setDataAndType(uri, "application/vnd.android.package-archive");
        startActivity(localIntent);
    }
  1. 手动配置智能安装服务 跳转辅助功能的配置界面,引导用户开启智能安装服务。
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);

总结

智能安装是一种妥协的方案,在没有 Root 和安装权限的情况下,确实解放了用户的拇指。看看市面上的应用,大部分都采用了这种方法。应用市场使用智能安装可以理解,视频浏览器工具一类不相干的应用也要开启?我真是呵呵了。

带有智能安装的应用